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.

smhi.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. /* global WeatherProvider, WeatherObject */
  2. /* Magic Mirror
  3. * Module: Weather
  4. * Provider: SMHI
  5. *
  6. * By BuXXi https://github.com/buxxi
  7. * MIT Licensed
  8. *
  9. * This class is a provider for SMHI (Sweden only). Metric system is the only
  10. * supported unit.
  11. */
  12. WeatherProvider.register("smhi", {
  13. providerName: "SMHI",
  14. // Set the default config properties that is specific to this provider
  15. defaults: {
  16. lat: 0,
  17. lon: 0,
  18. precipitationValue: "pmedian"
  19. },
  20. /**
  21. * Implements method in interface for fetching current weather
  22. */
  23. fetchCurrentWeather() {
  24. this.fetchData(this.getURL())
  25. .then((data) => {
  26. let closest = this.getClosestToCurrentTime(data.timeSeries);
  27. let coordinates = this.resolveCoordinates(data);
  28. let weatherObject = this.convertWeatherDataToObject(closest, coordinates);
  29. this.setFetchedLocation(`(${coordinates.lat},${coordinates.lon})`);
  30. this.setCurrentWeather(weatherObject);
  31. })
  32. .catch((error) => Log.error("Could not load data: " + error.message))
  33. .finally(() => this.updateAvailable());
  34. },
  35. /**
  36. * Implements method in interface for fetching a forecast.
  37. * Handling hourly forecast would be easy as not grouping by day but it seems really specific for one weather provider for now.
  38. */
  39. fetchWeatherForecast() {
  40. this.fetchData(this.getURL())
  41. .then((data) => {
  42. let coordinates = this.resolveCoordinates(data);
  43. let weatherObjects = this.convertWeatherDataGroupedByDay(data.timeSeries, coordinates);
  44. this.setFetchedLocation(`(${coordinates.lat},${coordinates.lon})`);
  45. this.setWeatherForecast(weatherObjects);
  46. })
  47. .catch((error) => Log.error("Could not load data: " + error.message))
  48. .finally(() => this.updateAvailable());
  49. },
  50. /**
  51. * Overrides method for setting config with checks for the precipitationValue being unset or invalid
  52. *
  53. * @param {object} config The configuration object
  54. */
  55. setConfig(config) {
  56. this.config = config;
  57. if (!config.precipitationValue || ["pmin", "pmean", "pmedian", "pmax"].indexOf(config.precipitationValue) === -1) {
  58. console.log("invalid or not set: " + config.precipitationValue);
  59. config.precipitationValue = this.defaults.precipitationValue;
  60. }
  61. },
  62. /**
  63. * Of all the times returned find out which one is closest to the current time, should be the first if the data isn't old.
  64. *
  65. * @param {object[]} times Array of time objects
  66. * @returns {object} The weatherdata closest to the current time
  67. */
  68. getClosestToCurrentTime(times) {
  69. let now = moment();
  70. let minDiff = undefined;
  71. for (const time of times) {
  72. let diff = Math.abs(moment(time.validTime).diff(now));
  73. if (!minDiff || diff < Math.abs(moment(minDiff.validTime).diff(now))) {
  74. minDiff = time;
  75. }
  76. }
  77. return minDiff;
  78. },
  79. /**
  80. * Get the forecast url for the configured coordinates
  81. *
  82. * @returns {string} the url for the specified coordinates
  83. */
  84. getURL() {
  85. let lon = this.config.lon;
  86. let lat = this.config.lat;
  87. return `https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/${lon}/lat/${lat}/data.json`;
  88. },
  89. /**
  90. * Converts the returned data into a WeatherObject with required properties set for both current weather and forecast.
  91. * The returned units is always in metric system.
  92. * Requires coordinates to determine if its daytime or nighttime to know which icon to use and also to set sunrise and sunset.
  93. *
  94. * @param {object} weatherData Weatherdata to convert
  95. * @param {object} coordinates Coordinates of the locations of the weather
  96. * @returns {WeatherObject} The converted weatherdata at the specified location
  97. */
  98. convertWeatherDataToObject(weatherData, coordinates) {
  99. // Weather data is only for Sweden and nobody in Sweden would use imperial
  100. let currentWeather = new WeatherObject("metric", "metric", "metric");
  101. currentWeather.date = moment(weatherData.validTime);
  102. currentWeather.updateSunTime(coordinates.lat, coordinates.lon);
  103. currentWeather.humidity = this.paramValue(weatherData, "r");
  104. currentWeather.temperature = this.paramValue(weatherData, "t");
  105. currentWeather.windSpeed = this.paramValue(weatherData, "ws");
  106. currentWeather.windDirection = this.paramValue(weatherData, "wd");
  107. currentWeather.weatherType = this.convertWeatherType(this.paramValue(weatherData, "Wsymb2"), currentWeather.isDayTime());
  108. // Determine the precipitation amount and category and update the
  109. // weatherObject with it, the valuetype to use can be configured or uses
  110. // median as default.
  111. let precipitationValue = this.paramValue(weatherData, this.config.precipitationValue);
  112. switch (this.paramValue(weatherData, "pcat")) {
  113. // 0 = No precipitation
  114. case 1: // Snow
  115. currentWeather.snow += precipitationValue;
  116. currentWeather.precipitation += precipitationValue;
  117. break;
  118. case 2: // Snow and rain, treat it as 50/50 snow and rain
  119. currentWeather.snow += precipitationValue / 2;
  120. currentWeather.rain += precipitationValue / 2;
  121. currentWeather.precipitation += precipitationValue;
  122. break;
  123. case 3: // Rain
  124. case 4: // Drizzle
  125. case 5: // Freezing rain
  126. case 6: // Freezing drizzle
  127. currentWeather.rain += precipitationValue;
  128. currentWeather.precipitation += precipitationValue;
  129. break;
  130. }
  131. return currentWeather;
  132. },
  133. /**
  134. * Takes all of the data points and converts it to one WeatherObject per day.
  135. *
  136. * @param {object[]} allWeatherData Array of weatherdata
  137. * @param {object} coordinates Coordinates of the locations of the weather
  138. * @returns {WeatherObject[]} Array of weatherobjects
  139. */
  140. convertWeatherDataGroupedByDay(allWeatherData, coordinates) {
  141. let currentWeather;
  142. let result = [];
  143. let allWeatherObjects = this.fillInGaps(allWeatherData).map((weatherData) => this.convertWeatherDataToObject(weatherData, coordinates));
  144. let dayWeatherTypes = [];
  145. for (const weatherObject of allWeatherObjects) {
  146. //If its the first object or if a day change we need to reset the summary object
  147. if (!currentWeather || !currentWeather.date.isSame(weatherObject.date, "day")) {
  148. currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
  149. dayWeatherTypes = [];
  150. currentWeather.date = weatherObject.date;
  151. currentWeather.minTemperature = Infinity;
  152. currentWeather.maxTemperature = -Infinity;
  153. currentWeather.snow = 0;
  154. currentWeather.rain = 0;
  155. currentWeather.precipitation = 0;
  156. result.push(currentWeather);
  157. }
  158. //Keep track of what icons has been used for each hour of daytime and use the middle one for the forecast
  159. if (weatherObject.isDayTime()) {
  160. dayWeatherTypes.push(weatherObject.weatherType);
  161. }
  162. if (dayWeatherTypes.length > 0) {
  163. currentWeather.weatherType = dayWeatherTypes[Math.floor(dayWeatherTypes.length / 2)];
  164. } else {
  165. currentWeather.weatherType = weatherObject.weatherType;
  166. }
  167. //All other properties is either a sum, min or max of each hour
  168. currentWeather.minTemperature = Math.min(currentWeather.minTemperature, weatherObject.temperature);
  169. currentWeather.maxTemperature = Math.max(currentWeather.maxTemperature, weatherObject.temperature);
  170. currentWeather.snow += weatherObject.snow;
  171. currentWeather.rain += weatherObject.rain;
  172. currentWeather.precipitation += weatherObject.precipitation;
  173. }
  174. return result;
  175. },
  176. /**
  177. * Resolve coordinates from the response data (probably preferably to use
  178. * this if it's not matching the config values exactly)
  179. *
  180. * @param {object} data Response data from the weather service
  181. * @returns {{lon, lat}} the lat/long coordinates of the data
  182. */
  183. resolveCoordinates(data) {
  184. return { lat: data.geometry.coordinates[0][1], lon: data.geometry.coordinates[0][0] };
  185. },
  186. /**
  187. * The distance between the data points is increasing in the data the more distant the prediction is.
  188. * Find these gaps and fill them with the previous hours data to make the data returned a complete set.
  189. *
  190. * @param {object[]} data Response data from the weather service
  191. * @returns {object[]} Given data with filled gaps
  192. */
  193. fillInGaps(data) {
  194. let result = [];
  195. for (let i = 1; i < data.length; i++) {
  196. let to = moment(data[i].validTime);
  197. let from = moment(data[i - 1].validTime);
  198. let hours = moment.duration(to.diff(from)).asHours();
  199. // For each hour add a datapoint but change the validTime
  200. for (let j = 0; j < hours; j++) {
  201. let current = Object.assign({}, data[i]);
  202. current.validTime = from.clone().add(j, "hours").toISOString();
  203. result.push(current);
  204. }
  205. }
  206. return result;
  207. },
  208. /**
  209. * Helper method to get a property from the returned data set.
  210. *
  211. * @param {object} currentWeatherData Weatherdata to get from
  212. * @param {string} name The name of the property
  213. * @returns {*} The value of the property in the weatherdata
  214. */
  215. paramValue(currentWeatherData, name) {
  216. return currentWeatherData.parameters.filter((p) => p.name === name).flatMap((p) => p.values)[0];
  217. },
  218. /**
  219. * Map the icon value from SMHI to an icon that MagicMirror understands.
  220. * Uses different icons depending if its daytime or nighttime.
  221. * SMHI's description of what the numeric value means is the comment after the case.
  222. *
  223. * @param {number} input The SMHI icon value
  224. * @param {boolean} isDayTime True if the icon should be for daytime, false for nighttime
  225. * @returns {string} The icon name for the MagicMirror
  226. */
  227. convertWeatherType(input, isDayTime) {
  228. switch (input) {
  229. case 1:
  230. return isDayTime ? "day-sunny" : "night-clear"; // Clear sky
  231. case 2:
  232. return isDayTime ? "day-sunny-overcast" : "night-partly-cloudy"; // Nearly clear sky
  233. case 3:
  234. return isDayTime ? "day-cloudy" : "night-cloudy"; // Variable cloudiness
  235. case 4:
  236. return isDayTime ? "day-cloudy" : "night-cloudy"; // Halfclear sky
  237. case 5:
  238. return "cloudy"; // Cloudy sky
  239. case 6:
  240. return "cloudy"; // Overcast
  241. case 7:
  242. return "fog"; // Fog
  243. case 8:
  244. return "showers"; // Light rain showers
  245. case 9:
  246. return "showers"; // Moderate rain showers
  247. case 10:
  248. return "showers"; // Heavy rain showers
  249. case 11:
  250. return "thunderstorm"; // Thunderstorm
  251. case 12:
  252. return "sleet"; // Light sleet showers
  253. case 13:
  254. return "sleet"; // Moderate sleet showers
  255. case 14:
  256. return "sleet"; // Heavy sleet showers
  257. case 15:
  258. return "snow"; // Light snow showers
  259. case 16:
  260. return "snow"; // Moderate snow showers
  261. case 17:
  262. return "snow"; // Heavy snow showers
  263. case 18:
  264. return "rain"; // Light rain
  265. case 19:
  266. return "rain"; // Moderate rain
  267. case 20:
  268. return "rain"; // Heavy rain
  269. case 21:
  270. return "thunderstorm"; // Thunder
  271. case 22:
  272. return "sleet"; // Light sleet
  273. case 23:
  274. return "sleet"; // Moderate sleet
  275. case 24:
  276. return "sleet"; // Heavy sleet
  277. case 25:
  278. return "snow"; // Light snowfall
  279. case 26:
  280. return "snow"; // Moderate snowfall
  281. case 27:
  282. return "snow"; // Heavy snowfall
  283. default:
  284. return "";
  285. }
  286. }
  287. });