Software zum Installieren eines Smart-Mirror Frameworks , zum Nutzen von hochschulrelevanten Informationen, auf einem Raspberry-Pi.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

weatherforecast.js 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. /* eslint-disable */
  2. /* Magic Mirror
  3. * Module: WeatherForecast
  4. *
  5. * By Michael Teeuw https://michaelteeuw.nl
  6. * MIT Licensed.
  7. *
  8. * This module is deprecated. Any additional feature will no longer be merged.
  9. */
  10. Module.register("weatherforecast", {
  11. // Default module config.
  12. defaults: {
  13. location: false,
  14. locationID: false,
  15. lat: false,
  16. lon: false,
  17. appid: "",
  18. units: config.units,
  19. maxNumberOfDays: 7,
  20. showRainAmount: false,
  21. updateInterval: 10 * 60 * 1000, // every 10 minutes
  22. animationSpeed: 1000,
  23. timeFormat: config.timeFormat,
  24. lang: config.language,
  25. decimalSymbol: ".",
  26. fade: true,
  27. fadePoint: 0.25, // Start on 1/4th of the list.
  28. colored: false,
  29. scale: false,
  30. initialLoadDelay: 2500, // 2.5 seconds delay. This delay is used to keep the OpenWeather API happy.
  31. retryDelay: 2500,
  32. apiVersion: "2.5",
  33. apiBase: "https://api.openweathermap.org/data/",
  34. forecastEndpoint: "forecast/daily",
  35. excludes: false,
  36. appendLocationNameToHeader: true,
  37. calendarClass: "calendar",
  38. tableClass: "small",
  39. roundTemp: false,
  40. iconTable: {
  41. "01d": "wi-day-sunny",
  42. "02d": "wi-day-cloudy",
  43. "03d": "wi-cloudy",
  44. "04d": "wi-cloudy-windy",
  45. "09d": "wi-showers",
  46. "10d": "wi-rain",
  47. "11d": "wi-thunderstorm",
  48. "13d": "wi-snow",
  49. "50d": "wi-fog",
  50. "01n": "wi-night-clear",
  51. "02n": "wi-night-cloudy",
  52. "03n": "wi-night-cloudy",
  53. "04n": "wi-night-cloudy",
  54. "09n": "wi-night-showers",
  55. "10n": "wi-night-rain",
  56. "11n": "wi-night-thunderstorm",
  57. "13n": "wi-night-snow",
  58. "50n": "wi-night-alt-cloudy-windy"
  59. }
  60. },
  61. // create a variable for the first upcoming calendar event. Used if no location is specified.
  62. firstEvent: false,
  63. // create a variable to hold the location name based on the API result.
  64. fetchedLocationName: "",
  65. // Define required scripts.
  66. getScripts: function () {
  67. return ["moment.js"];
  68. },
  69. // Define required scripts.
  70. getStyles: function () {
  71. return ["weather-icons.css", "weatherforecast.css"];
  72. },
  73. // Define required translations.
  74. getTranslations: function () {
  75. // The translations for the default modules are defined in the core translation files.
  76. // Therefor we can just return false. Otherwise we should have returned a dictionary.
  77. // If you're trying to build your own module including translations, check out the documentation.
  78. return false;
  79. },
  80. // Define start sequence.
  81. start: function () {
  82. Log.info("Starting module: " + this.name);
  83. // Set locale.
  84. moment.locale(config.language);
  85. this.forecast = [];
  86. this.loaded = false;
  87. this.scheduleUpdate(this.config.initialLoadDelay);
  88. this.updateTimer = null;
  89. },
  90. // Override dom generator.
  91. getDom: function () {
  92. var wrapper = document.createElement("div");
  93. if (this.config.appid === "" || this.config.appid === "YOUR_OPENWEATHER_API_KEY") {
  94. wrapper.innerHTML = "Please set the correct openweather <i>appid</i> in the config for module: " + this.name + ".";
  95. wrapper.className = "dimmed light small";
  96. return wrapper;
  97. }
  98. if (!this.loaded) {
  99. wrapper.innerHTML = this.translate("LOADING");
  100. wrapper.className = "dimmed light small";
  101. return wrapper;
  102. }
  103. var table = document.createElement("table");
  104. table.className = this.config.tableClass;
  105. for (var f in this.forecast) {
  106. var forecast = this.forecast[f];
  107. var row = document.createElement("tr");
  108. if (this.config.colored) {
  109. row.className = "colored";
  110. }
  111. table.appendChild(row);
  112. var dayCell = document.createElement("td");
  113. dayCell.className = "day";
  114. dayCell.innerHTML = forecast.day;
  115. row.appendChild(dayCell);
  116. var iconCell = document.createElement("td");
  117. iconCell.className = "bright weather-icon";
  118. row.appendChild(iconCell);
  119. var icon = document.createElement("span");
  120. icon.className = "wi weathericon " + forecast.icon;
  121. iconCell.appendChild(icon);
  122. var degreeLabel = "";
  123. if (this.config.units === "metric" || this.config.units === "imperial") {
  124. degreeLabel += "°";
  125. }
  126. if (this.config.scale) {
  127. switch (this.config.units) {
  128. case "metric":
  129. degreeLabel += "C";
  130. break;
  131. case "imperial":
  132. degreeLabel += "F";
  133. break;
  134. case "default":
  135. degreeLabel = "K";
  136. break;
  137. }
  138. }
  139. if (this.config.decimalSymbol === "" || this.config.decimalSymbol === " ") {
  140. this.config.decimalSymbol = ".";
  141. }
  142. var maxTempCell = document.createElement("td");
  143. maxTempCell.innerHTML = forecast.maxTemp.replace(".", this.config.decimalSymbol) + degreeLabel;
  144. maxTempCell.className = "align-right bright max-temp";
  145. row.appendChild(maxTempCell);
  146. var minTempCell = document.createElement("td");
  147. minTempCell.innerHTML = forecast.minTemp.replace(".", this.config.decimalSymbol) + degreeLabel;
  148. minTempCell.className = "align-right min-temp";
  149. row.appendChild(minTempCell);
  150. if (this.config.showRainAmount) {
  151. var rainCell = document.createElement("td");
  152. if (isNaN(forecast.rain)) {
  153. rainCell.innerHTML = "";
  154. } else {
  155. if (config.units !== "imperial") {
  156. rainCell.innerHTML = parseFloat(forecast.rain).toFixed(1).replace(".", this.config.decimalSymbol) + " mm";
  157. } else {
  158. rainCell.innerHTML = (parseFloat(forecast.rain) / 25.4).toFixed(2).replace(".", this.config.decimalSymbol) + " in";
  159. }
  160. }
  161. rainCell.className = "align-right bright rain";
  162. row.appendChild(rainCell);
  163. }
  164. if (this.config.fade && this.config.fadePoint < 1) {
  165. if (this.config.fadePoint < 0) {
  166. this.config.fadePoint = 0;
  167. }
  168. var startingPoint = this.forecast.length * this.config.fadePoint;
  169. var steps = this.forecast.length - startingPoint;
  170. if (f >= startingPoint) {
  171. var currentStep = f - startingPoint;
  172. row.style.opacity = 1 - (1 / steps) * currentStep;
  173. }
  174. }
  175. }
  176. return table;
  177. },
  178. // Override getHeader method.
  179. getHeader: function () {
  180. if (this.config.appendLocationNameToHeader) {
  181. if (this.data.header) return this.data.header + " " + this.fetchedLocationName;
  182. else return this.fetchedLocationName;
  183. }
  184. return this.data.header ? this.data.header : "";
  185. },
  186. // Override notification handler.
  187. notificationReceived: function (notification, payload, sender) {
  188. if (notification === "DOM_OBJECTS_CREATED") {
  189. if (this.config.appendLocationNameToHeader) {
  190. this.hide(0, { lockString: this.identifier });
  191. }
  192. }
  193. if (notification === "CALENDAR_EVENTS") {
  194. var senderClasses = sender.data.classes.toLowerCase().split(" ");
  195. if (senderClasses.indexOf(this.config.calendarClass.toLowerCase()) !== -1) {
  196. this.firstEvent = false;
  197. for (var e in payload) {
  198. var event = payload[e];
  199. if (event.location || event.geo) {
  200. this.firstEvent = event;
  201. //Log.log("First upcoming event with location: ", event);
  202. break;
  203. }
  204. }
  205. }
  206. }
  207. },
  208. /* updateWeather(compliments)
  209. * Requests new data from openweather.org.
  210. * Calls processWeather on successful response.
  211. */
  212. updateWeather: function () {
  213. if (this.config.appid === "") {
  214. Log.error("WeatherForecast: APPID not set!");
  215. return;
  216. }
  217. var url = this.config.apiBase + this.config.apiVersion + "/" + this.config.forecastEndpoint + this.getParams();
  218. var self = this;
  219. var retry = true;
  220. var weatherRequest = new XMLHttpRequest();
  221. weatherRequest.open("GET", url, true);
  222. weatherRequest.onreadystatechange = function () {
  223. if (this.readyState === 4) {
  224. if (this.status === 200) {
  225. self.processWeather(JSON.parse(this.response));
  226. } else if (this.status === 401) {
  227. self.updateDom(self.config.animationSpeed);
  228. if (self.config.forecastEndpoint === "forecast/daily") {
  229. self.config.forecastEndpoint = "forecast";
  230. Log.warn(self.name + ": Your AppID does not support long term forecasts. Switching to fallback endpoint.");
  231. }
  232. retry = true;
  233. } else {
  234. Log.error(self.name + ": Could not load weather.");
  235. }
  236. if (retry) {
  237. self.scheduleUpdate(self.loaded ? -1 : self.config.retryDelay);
  238. }
  239. }
  240. };
  241. weatherRequest.send();
  242. },
  243. /* getParams(compliments)
  244. * Generates an url with api parameters based on the config.
  245. *
  246. * return String - URL params.
  247. */
  248. getParams: function () {
  249. var params = "?";
  250. if (this.config.locationID) {
  251. params += "id=" + this.config.locationID;
  252. } else if (this.config.lat && this.config.lon) {
  253. params += "lat=" + this.config.lat + "&lon=" + this.config.lon;
  254. } else if (this.config.location) {
  255. params += "q=" + this.config.location;
  256. } else if (this.firstEvent && this.firstEvent.geo) {
  257. params += "lat=" + this.firstEvent.geo.lat + "&lon=" + this.firstEvent.geo.lon;
  258. } else if (this.firstEvent && this.firstEvent.location) {
  259. params += "q=" + this.firstEvent.location;
  260. } else {
  261. this.hide(this.config.animationSpeed, { lockString: this.identifier });
  262. return;
  263. }
  264. let numberOfDays;
  265. if (this.config.forecastEndpoint === "forecast") {
  266. numberOfDays = this.config.maxNumberOfDays < 1 || this.config.maxNumberOfDays > 5 ? 5 : this.config.maxNumberOfDays;
  267. // don't get forecasts for the next day, as it would not represent the whole day
  268. numberOfDays = numberOfDays * 8 - (Math.round(new Date().getHours() / 3) % 8);
  269. } else {
  270. numberOfDays = this.config.maxNumberOfDays < 1 || this.config.maxNumberOfDays > 17 ? 7 : this.config.maxNumberOfDays;
  271. }
  272. params += "&cnt=" + numberOfDays;
  273. params += "&exclude=" + this.config.excludes;
  274. params += "&units=" + this.config.units;
  275. params += "&lang=" + this.config.lang;
  276. params += "&APPID=" + this.config.appid;
  277. return params;
  278. },
  279. /*
  280. * parserDataWeather(data)
  281. *
  282. * Use the parse to keep the same struct between daily and forecast Endpoint
  283. * from openweather.org
  284. *
  285. */
  286. parserDataWeather: function (data) {
  287. if (data.hasOwnProperty("main")) {
  288. data["temp"] = { min: data.main.temp_min, max: data.main.temp_max };
  289. }
  290. return data;
  291. },
  292. /* processWeather(data)
  293. * Uses the received data to set the various values.
  294. *
  295. * argument data object - Weather information received form openweather.org.
  296. */
  297. processWeather: function (data, momenttz) {
  298. let mom = momenttz ? momenttz : moment; // Exception last.
  299. // Forcast16 (paid) API endpoint provides this data. Onecall endpoint
  300. // does not.
  301. if (data.city) {
  302. this.fetchedLocationName = data.city.name + ", " + data.city.country;
  303. } else if (this.config.location) {
  304. this.fetchedLocationName = this.config.location;
  305. } else {
  306. this.fetchedLocationName = "Unknown";
  307. }
  308. this.forecast = [];
  309. var lastDay = null;
  310. var forecastData = {};
  311. var dayStarts = 8;
  312. var dayEnds = 17;
  313. if (data.city && data.city.sunrise && data.city.sunset) {
  314. dayStarts = new Date(mom.unix(data.city.sunrise).locale("en").format("YYYY/MM/DD HH:mm:ss")).getHours();
  315. dayEnds = new Date(mom.unix(data.city.sunset).locale("en").format("YYYY/MM/DD HH:mm:ss")).getHours();
  316. }
  317. // Handle different structs between forecast16 and onecall endpoints
  318. var forecastList = null;
  319. if (data.list) {
  320. forecastList = data.list;
  321. } else if (data.daily) {
  322. forecastList = data.daily;
  323. } else {
  324. Log.error("Unexpected forecast data");
  325. return undefined;
  326. }
  327. for (var i = 0, count = forecastList.length; i < count; i++) {
  328. var forecast = forecastList[i];
  329. forecast = this.parserDataWeather(forecast); // hack issue #1017
  330. var day;
  331. var hour;
  332. if (forecast.dt_txt) {
  333. day = mom(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss").format("ddd");
  334. hour = new Date(mom(forecast.dt_txt).locale("en").format("YYYY-MM-DD HH:mm:ss")).getHours();
  335. } else {
  336. day = mom(forecast.dt, "X").format("ddd");
  337. hour = new Date(mom(forecast.dt, "X")).getHours();
  338. }
  339. if (day !== lastDay) {
  340. forecastData = {
  341. day: day,
  342. icon: this.config.iconTable[forecast.weather[0].icon],
  343. maxTemp: this.roundValue(forecast.temp.max),
  344. minTemp: this.roundValue(forecast.temp.min),
  345. rain: this.processRain(forecast, forecastList, mom)
  346. };
  347. this.forecast.push(forecastData);
  348. lastDay = day;
  349. // Stop processing when maxNumberOfDays is reached
  350. if (this.forecast.length === this.config.maxNumberOfDays) {
  351. break;
  352. }
  353. } else {
  354. //Log.log("Compare max: ", forecast.temp.max, parseFloat(forecastData.maxTemp));
  355. forecastData.maxTemp = forecast.temp.max > parseFloat(forecastData.maxTemp) ? this.roundValue(forecast.temp.max) : forecastData.maxTemp;
  356. //Log.log("Compare min: ", forecast.temp.min, parseFloat(forecastData.minTemp));
  357. forecastData.minTemp = forecast.temp.min < parseFloat(forecastData.minTemp) ? this.roundValue(forecast.temp.min) : forecastData.minTemp;
  358. // Since we don't want an icon from the start of the day (in the middle of the night)
  359. // we update the icon as long as it's somewhere during the day.
  360. if (hour > dayStarts && hour < dayEnds) {
  361. forecastData.icon = this.config.iconTable[forecast.weather[0].icon];
  362. }
  363. }
  364. }
  365. //Log.log(this.forecast);
  366. this.show(this.config.animationSpeed, { lockString: this.identifier });
  367. this.loaded = true;
  368. this.updateDom(this.config.animationSpeed);
  369. },
  370. /* scheduleUpdate()
  371. * Schedule next update.
  372. *
  373. * argument delay number - Milliseconds before next update. If empty, this.config.updateInterval is used.
  374. */
  375. scheduleUpdate: function (delay) {
  376. var nextLoad = this.config.updateInterval;
  377. if (typeof delay !== "undefined" && delay >= 0) {
  378. nextLoad = delay;
  379. }
  380. var self = this;
  381. clearTimeout(this.updateTimer);
  382. this.updateTimer = setTimeout(function () {
  383. self.updateWeather();
  384. }, nextLoad);
  385. },
  386. /* ms2Beaufort(ms)
  387. * Converts m2 to beaufort (windspeed).
  388. *
  389. * see:
  390. * https://www.spc.noaa.gov/faq/tornado/beaufort.html
  391. * https://en.wikipedia.org/wiki/Beaufort_scale#Modern_scale
  392. *
  393. * argument ms number - Windspeed in m/s.
  394. *
  395. * return number - Windspeed in beaufort.
  396. */
  397. ms2Beaufort: function (ms) {
  398. var kmh = (ms * 60 * 60) / 1000;
  399. var speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000];
  400. for (var beaufort in speeds) {
  401. var speed = speeds[beaufort];
  402. if (speed > kmh) {
  403. return beaufort;
  404. }
  405. }
  406. return 12;
  407. },
  408. /* function(temperature)
  409. * Rounds a temperature to 1 decimal or integer (depending on config.roundTemp).
  410. *
  411. * argument temperature number - Temperature.
  412. *
  413. * return string - Rounded Temperature.
  414. */
  415. roundValue: function (temperature) {
  416. var decimals = this.config.roundTemp ? 0 : 1;
  417. var roundValue = parseFloat(temperature).toFixed(decimals);
  418. return roundValue === "-0" ? 0 : roundValue;
  419. },
  420. /* processRain(forecast, allForecasts)
  421. * Calculates the amount of rain for a whole day even if long term forecasts isn't available for the appid.
  422. *
  423. * When using the the fallback endpoint forecasts are provided in 3h intervals and the rain-property is an object instead of number.
  424. * That object has a property "3h" which contains the amount of rain since the previous forecast in the list.
  425. * This code finds all forecasts that is for the same day and sums the amount of rain and returns that.
  426. */
  427. processRain: function (forecast, allForecasts, momenttz) {
  428. let mom = momenttz ? momenttz : moment; // Exception last.
  429. //If the amount of rain actually is a number, return it
  430. if (!isNaN(forecast.rain)) {
  431. return forecast.rain;
  432. }
  433. //Find all forecasts that is for the same day
  434. var checkDateTime = forecast.dt_txt ? mom(forecast.dt_txt, "YYYY-MM-DD hh:mm:ss") : mom(forecast.dt, "X");
  435. var daysForecasts = allForecasts.filter(function (item) {
  436. var itemDateTime = item.dt_txt ? mom(item.dt_txt, "YYYY-MM-DD hh:mm:ss") : mom(item.dt, "X");
  437. return itemDateTime.isSame(checkDateTime, "day") && item.rain instanceof Object;
  438. });
  439. //If no rain this day return undefined so it wont be displayed for this day
  440. if (daysForecasts.length === 0) {
  441. return undefined;
  442. }
  443. //Summarize all the rain from the matching days
  444. return daysForecasts
  445. .map(function (item) {
  446. return Object.values(item.rain)[0];
  447. })
  448. .reduce(function (a, b) {
  449. return a + b;
  450. }, 0);
  451. }
  452. });