mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 17:01:08 -04:00
## [2.27.0] - 2024-04-01 Thanks to: @bugsounet, @crazyscot, @illimarkangur, @jkriegshauser, @khassel, @KristjanESPERANTO, @Paranoid93, @rejas, @sdetweil and @vppencilsharpener. This release marks the first release without Michael Teeuw (@michmich). A very special thanks to him for creating MagicMirror and leading the project for so many years. For more info, please read the following post: [A New Chapter for MagicMirror: The Community Takes the Lead](https://forum.magicmirror.builders/topic/18329/a-new-chapter-for-magicmirror-the-community-takes-the-lead). ### Added - Output of system information to the console for troubleshooting (#3328 and #3337), ignore errors under aarch64 (#3349) - [chore] Add `eslint-plugin-package-json` to lint the `package.json` files (#3368) - [weather] `showHumidity` config is now a string describing where to show this element. Supported values: "wind", "temp", "feelslike", "below", "none". (#3330) - electron-rebuild test suite for electron and 3rd party modules compatibility (#3392) - Create MM² icon and attach it to electron process (#3407) ### Updated - Update updatenotification (update_helper.js): Recode with pm2 library (#3332) - Removing lodash dependency by replacing merge by spread operator (#3339) - Use node prefix for build-in modules (#3340) - Rework logging colors (#3350) - Update pm2 to v5.3.1 with no allow-ghsas (#3364) - [chore] Update husky and let lint-staged fix ESLint issues - [chore] Update dependencies including electron to v29 (#3357) and node-ical - Update translations for estonian (#3371) - Update electron to v29 and update other dependencies - [calendar] fullDay events over several days now show the left days from the first day on and 'today' on the last day - Update layout of current weather indoor values ### Fixed - Correct apibase of weathergov weatherprovider to match documentation (#2926) - Worked around several issues in the RRULE library that were causing deleted calender events to still show, some initial and recurring events to not show, and some event times to be off an hour. (#3291) - Skip changelog requirement when running tests for dependency updates (#3320) - Display precipitation probability when it is 0% instead of blank/empty (#3345) - [newsfeed] Suppress unsightly animation cases when there are 0 or 1 active news items (#3336) - [newsfeed] Always compute the feed item URL using the same helper function (#3336) - Ignore all custom css files (#3359) - [newsfeed] Fix newsfeed stall issue introduced by #3336 (#3361) - Changed `log.debug` to `log.log` in `app.js` where logLevel is not set because config is not loaded at this time (#3353) - [calendar] deny fetch interval < 60000 and set 60000 in this case (prevent fetch loop failed) (#3382) - added message in case where config.js is missing the module.export line PR #3383 - Fixed an issue where recurring events could extend past their recurrence end date (#3393) - Don't display any `npm WARN <....>` on install (#3399) - Fixed move suncalc dependency to production from dev, as it is used by clock module - [compliments] Fix mirror not responding anymore when no compliments are to be shown (#3385) ### Deleted - Unneeded file headers (#3358) --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Michael Teeuw <michael@xonaymedia.nl> Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Karsten Hassel <hassel@gmx.de> Co-authored-by: Ross Younger <crazyscot@gmail.com> Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr> Co-authored-by: jkriegshauser <joshuakr@nvidia.com> Co-authored-by: illimarkangur <116028111+illimarkangur@users.noreply.github.com> Co-authored-by: sam detweiler <sdetweil@gmail.com> Co-authored-by: vppencilsharpener <tim.pray@gmail.com> Co-authored-by: Paranoid93 <6515818+Paranoid93@users.noreply.github.com>
328 lines
12 KiB
JavaScript
328 lines
12 KiB
JavaScript
/* global WeatherProvider, WeatherObject */
|
|
|
|
/* This class is a provider for SMHI (Sweden only).
|
|
* Metric system is the only supported unit,
|
|
* see https://www.smhi.se/
|
|
*/
|
|
WeatherProvider.register("smhi", {
|
|
providerName: "SMHI",
|
|
|
|
// Set the default config properties that is specific to this provider
|
|
defaults: {
|
|
lat: 0, // Cant have more than 6 digits
|
|
lon: 0, // Cant have more than 6 digits
|
|
precipitationValue: "pmedian",
|
|
location: false
|
|
},
|
|
|
|
/**
|
|
* Implements method in interface for fetching current weather.
|
|
*/
|
|
fetchCurrentWeather () {
|
|
this.fetchData(this.getURL())
|
|
.then((data) => {
|
|
const closest = this.getClosestToCurrentTime(data.timeSeries);
|
|
const coordinates = this.resolveCoordinates(data);
|
|
const weatherObject = this.convertWeatherDataToObject(closest, coordinates);
|
|
this.setFetchedLocation(this.config.location || `(${coordinates.lat},${coordinates.lon})`);
|
|
this.setCurrentWeather(weatherObject);
|
|
})
|
|
.catch((error) => Log.error(`Could not load data: ${error.message}`))
|
|
.finally(() => this.updateAvailable());
|
|
},
|
|
|
|
/**
|
|
* Implements method in interface for fetching a multi-day forecast.
|
|
*/
|
|
fetchWeatherForecast () {
|
|
this.fetchData(this.getURL())
|
|
.then((data) => {
|
|
const coordinates = this.resolveCoordinates(data);
|
|
const weatherObjects = this.convertWeatherDataGroupedBy(data.timeSeries, coordinates);
|
|
this.setFetchedLocation(this.config.location || `(${coordinates.lat},${coordinates.lon})`);
|
|
this.setWeatherForecast(weatherObjects);
|
|
})
|
|
.catch((error) => Log.error(`Could not load data: ${error.message}`))
|
|
.finally(() => this.updateAvailable());
|
|
},
|
|
|
|
/**
|
|
* Implements method in interface for fetching hourly forecasts.
|
|
*/
|
|
fetchWeatherHourly () {
|
|
this.fetchData(this.getURL())
|
|
.then((data) => {
|
|
const coordinates = this.resolveCoordinates(data);
|
|
const weatherObjects = this.convertWeatherDataGroupedBy(data.timeSeries, coordinates, "hour");
|
|
this.setFetchedLocation(this.config.location || `(${coordinates.lat},${coordinates.lon})`);
|
|
this.setWeatherHourly(weatherObjects);
|
|
})
|
|
.catch((error) => Log.error(`Could not load data: ${error.message}`))
|
|
.finally(() => this.updateAvailable());
|
|
},
|
|
|
|
/**
|
|
* Overrides method for setting config with checks for the precipitationValue being unset or invalid
|
|
* @param {object} config The configuration object
|
|
*/
|
|
setConfig (config) {
|
|
this.config = config;
|
|
if (!config.precipitationValue || ["pmin", "pmean", "pmedian", "pmax"].indexOf(config.precipitationValue) === -1) {
|
|
Log.log(`invalid or not set: ${config.precipitationValue}`);
|
|
config.precipitationValue = this.defaults.precipitationValue;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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.
|
|
* @param {object[]} times Array of time objects
|
|
* @returns {object} The weatherdata closest to the current time
|
|
*/
|
|
getClosestToCurrentTime (times) {
|
|
let now = moment();
|
|
let minDiff = undefined;
|
|
for (const time of times) {
|
|
let diff = Math.abs(moment(time.validTime).diff(now));
|
|
if (!minDiff || diff < Math.abs(moment(minDiff.validTime).diff(now))) {
|
|
minDiff = time;
|
|
}
|
|
}
|
|
return minDiff;
|
|
},
|
|
|
|
/**
|
|
* Get the forecast url for the configured coordinates
|
|
* @returns {string} the url for the specified coordinates
|
|
*/
|
|
getURL () {
|
|
const formatter = new Intl.NumberFormat("en-US", {
|
|
minimumFractionDigits: 6,
|
|
maximumFractionDigits: 6
|
|
});
|
|
const lon = formatter.format(this.config.lon);
|
|
const lat = formatter.format(this.config.lat);
|
|
return `https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/${lon}/lat/${lat}/data.json`;
|
|
},
|
|
|
|
/**
|
|
* Calculates the apparent temperature based on known atmospheric data.
|
|
* @param {object} weatherData Weatherdata to use for the calculation
|
|
* @returns {number} The apparent temperature
|
|
*/
|
|
calculateApparentTemperature (weatherData) {
|
|
const Ta = this.paramValue(weatherData, "t");
|
|
const rh = this.paramValue(weatherData, "r");
|
|
const ws = this.paramValue(weatherData, "ws");
|
|
const p = (rh / 100) * 6.105 * Math.E * ((17.27 * Ta) / (237.7 + Ta));
|
|
|
|
return Ta + 0.33 * p - 0.7 * ws - 4;
|
|
},
|
|
|
|
/**
|
|
* Converts the returned data into a WeatherObject with required properties set for both current weather and forecast.
|
|
* The returned units is always in metric system.
|
|
* Requires coordinates to determine if its daytime or nighttime to know which icon to use and also to set sunrise and sunset.
|
|
* @param {object} weatherData Weatherdata to convert
|
|
* @param {object} coordinates Coordinates of the locations of the weather
|
|
* @returns {WeatherObject} The converted weatherdata at the specified location
|
|
*/
|
|
convertWeatherDataToObject (weatherData, coordinates) {
|
|
let currentWeather = new WeatherObject();
|
|
|
|
currentWeather.date = moment(weatherData.validTime);
|
|
currentWeather.updateSunTime(coordinates.lat, coordinates.lon);
|
|
currentWeather.humidity = this.paramValue(weatherData, "r");
|
|
currentWeather.temperature = this.paramValue(weatherData, "t");
|
|
currentWeather.windSpeed = this.paramValue(weatherData, "ws");
|
|
currentWeather.windFromDirection = this.paramValue(weatherData, "wd");
|
|
currentWeather.weatherType = this.convertWeatherType(this.paramValue(weatherData, "Wsymb2"), currentWeather.isDayTime());
|
|
currentWeather.feelsLikeTemp = this.calculateApparentTemperature(weatherData);
|
|
|
|
// Determine the precipitation amount and category and update the
|
|
// weatherObject with it, the valuetype to use can be configured or uses
|
|
// median as default.
|
|
let precipitationValue = this.paramValue(weatherData, this.config.precipitationValue);
|
|
switch (this.paramValue(weatherData, "pcat")) {
|
|
// 0 = No precipitation
|
|
case 1: // Snow
|
|
currentWeather.snow += precipitationValue;
|
|
currentWeather.precipitationAmount += precipitationValue;
|
|
break;
|
|
case 2: // Snow and rain, treat it as 50/50 snow and rain
|
|
currentWeather.snow += precipitationValue / 2;
|
|
currentWeather.rain += precipitationValue / 2;
|
|
currentWeather.precipitationAmount += precipitationValue;
|
|
break;
|
|
case 3: // Rain
|
|
case 4: // Drizzle
|
|
case 5: // Freezing rain
|
|
case 6: // Freezing drizzle
|
|
currentWeather.rain += precipitationValue;
|
|
currentWeather.precipitationAmount += precipitationValue;
|
|
break;
|
|
}
|
|
|
|
return currentWeather;
|
|
},
|
|
|
|
/**
|
|
* Takes all the data points and converts it to one WeatherObject per day.
|
|
* @param {object[]} allWeatherData Array of weatherdata
|
|
* @param {object} coordinates Coordinates of the locations of the weather
|
|
* @param {string} groupBy The interval to use for grouping the data (day, hour)
|
|
* @returns {WeatherObject[]} Array of weatherobjects
|
|
*/
|
|
convertWeatherDataGroupedBy (allWeatherData, coordinates, groupBy = "day") {
|
|
let currentWeather;
|
|
let result = [];
|
|
|
|
let allWeatherObjects = this.fillInGaps(allWeatherData).map((weatherData) => this.convertWeatherDataToObject(weatherData, coordinates));
|
|
let dayWeatherTypes = [];
|
|
|
|
for (const weatherObject of allWeatherObjects) {
|
|
//If its the first object or if a day/hour change we need to reset the summary object
|
|
if (!currentWeather || !currentWeather.date.isSame(weatherObject.date, groupBy)) {
|
|
currentWeather = new WeatherObject();
|
|
dayWeatherTypes = [];
|
|
currentWeather.temperature = weatherObject.temperature;
|
|
currentWeather.date = weatherObject.date;
|
|
currentWeather.minTemperature = Infinity;
|
|
currentWeather.maxTemperature = -Infinity;
|
|
currentWeather.snow = 0;
|
|
currentWeather.rain = 0;
|
|
currentWeather.precipitationAmount = 0;
|
|
result.push(currentWeather);
|
|
}
|
|
|
|
//Keep track of what icons have been used for each hour of daytime and use the middle one for the forecast
|
|
if (weatherObject.isDayTime()) {
|
|
dayWeatherTypes.push(weatherObject.weatherType);
|
|
}
|
|
if (dayWeatherTypes.length > 0) {
|
|
currentWeather.weatherType = dayWeatherTypes[Math.floor(dayWeatherTypes.length / 2)];
|
|
} else {
|
|
currentWeather.weatherType = weatherObject.weatherType;
|
|
}
|
|
|
|
//All other properties is either a sum, min or max of each hour
|
|
currentWeather.minTemperature = Math.min(currentWeather.minTemperature, weatherObject.temperature);
|
|
currentWeather.maxTemperature = Math.max(currentWeather.maxTemperature, weatherObject.temperature);
|
|
currentWeather.snow += weatherObject.snow;
|
|
currentWeather.rain += weatherObject.rain;
|
|
currentWeather.precipitationAmount += weatherObject.precipitationAmount;
|
|
}
|
|
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* Resolve coordinates from the response data (probably preferably to use
|
|
* this if it's not matching the config values exactly)
|
|
* @param {object} data Response data from the weather service
|
|
* @returns {{lon, lat}} the lat/long coordinates of the data
|
|
*/
|
|
resolveCoordinates (data) {
|
|
return { lat: data.geometry.coordinates[0][1], lon: data.geometry.coordinates[0][0] };
|
|
},
|
|
|
|
/**
|
|
* The distance between the data points is increasing in the data the more distant the prediction is.
|
|
* Find these gaps and fill them with the previous hours data to make the data returned a complete set.
|
|
* @param {object[]} data Response data from the weather service
|
|
* @returns {object[]} Given data with filled gaps
|
|
*/
|
|
fillInGaps (data) {
|
|
let result = [];
|
|
for (let i = 1; i < data.length; i++) {
|
|
let to = moment(data[i].validTime);
|
|
let from = moment(data[i - 1].validTime);
|
|
let hours = moment.duration(to.diff(from)).asHours();
|
|
// For each hour add a datapoint but change the validTime
|
|
for (let j = 0; j < hours; j++) {
|
|
let current = Object.assign({}, data[i]);
|
|
current.validTime = from.clone().add(j, "hours").toISOString();
|
|
result.push(current);
|
|
}
|
|
}
|
|
return result;
|
|
},
|
|
|
|
/**
|
|
* Helper method to get a property from the returned data set.
|
|
* @param {object} currentWeatherData Weatherdata to get from
|
|
* @param {string} name The name of the property
|
|
* @returns {*} The value of the property in the weatherdata
|
|
*/
|
|
paramValue (currentWeatherData, name) {
|
|
return currentWeatherData.parameters.filter((p) => p.name === name).flatMap((p) => p.values)[0];
|
|
},
|
|
|
|
/**
|
|
* Map the icon value from SMHI to an icon that MagicMirror² understands.
|
|
* Uses different icons depending on if its daytime or nighttime.
|
|
* SMHI's description of what the numeric value means is the comment after the case.
|
|
* @param {number} input The SMHI icon value
|
|
* @param {boolean} isDayTime True if the icon should be for daytime, false for nighttime
|
|
* @returns {string} The icon name for the MagicMirror
|
|
*/
|
|
convertWeatherType (input, isDayTime) {
|
|
switch (input) {
|
|
case 1:
|
|
return isDayTime ? "day-sunny" : "night-clear"; // Clear sky
|
|
case 2:
|
|
return isDayTime ? "day-sunny-overcast" : "night-partly-cloudy"; // Nearly clear sky
|
|
case 3:
|
|
return isDayTime ? "day-cloudy" : "night-cloudy"; // Variable cloudiness
|
|
case 4:
|
|
return isDayTime ? "day-cloudy" : "night-cloudy"; // Halfclear sky
|
|
case 5:
|
|
return "cloudy"; // Cloudy sky
|
|
case 6:
|
|
return "cloudy"; // Overcast
|
|
case 7:
|
|
return "fog"; // Fog
|
|
case 8:
|
|
return "showers"; // Light rain showers
|
|
case 9:
|
|
return "showers"; // Moderate rain showers
|
|
case 10:
|
|
return "showers"; // Heavy rain showers
|
|
case 11:
|
|
return "thunderstorm"; // Thunderstorm
|
|
case 12:
|
|
return "sleet"; // Light sleet showers
|
|
case 13:
|
|
return "sleet"; // Moderate sleet showers
|
|
case 14:
|
|
return "sleet"; // Heavy sleet showers
|
|
case 15:
|
|
return "snow"; // Light snow showers
|
|
case 16:
|
|
return "snow"; // Moderate snow showers
|
|
case 17:
|
|
return "snow"; // Heavy snow showers
|
|
case 18:
|
|
return "rain"; // Light rain
|
|
case 19:
|
|
return "rain"; // Moderate rain
|
|
case 20:
|
|
return "rain"; // Heavy rain
|
|
case 21:
|
|
return "thunderstorm"; // Thunder
|
|
case 22:
|
|
return "sleet"; // Light sleet
|
|
case 23:
|
|
return "sleet"; // Moderate sleet
|
|
case 24:
|
|
return "sleet"; // Heavy sleet
|
|
case 25:
|
|
return "snow"; // Light snowfall
|
|
case 26:
|
|
return "snow"; // Moderate snowfall
|
|
case 27:
|
|
return "snow"; // Heavy snowfall
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
});
|